import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
dr14 = pd.read_csv('Dataset/SDSS_DR14.csv')
dr16 = pd.read_csv('Dataset/SDSS_DR16.csv')
dr17 = pd.read_csv('Dataset/SDSS_DR17.csv')
dr18 = pd.read_csv('Dataset/SDSS_DR18.csv')
dataframe = pd.concat([dr14, dr16, dr17, dr18], ignore_index=True, sort=False)
df = dataframe.dropna(axis=1)
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 310000 entries, 0 to 309999 Data columns (total 18 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 objid 310000 non-null object 1 ra 310000 non-null float64 2 dec 310000 non-null float64 3 u 310000 non-null float64 4 g 310000 non-null float64 5 r 310000 non-null float64 6 i 310000 non-null float64 7 z 310000 non-null float64 8 run 310000 non-null int64 9 rerun 310000 non-null int64 10 camcol 310000 non-null int64 11 field 310000 non-null int64 12 specobjid 310000 non-null object 13 class 310000 non-null object 14 redshift 310000 non-null float64 15 plate 310000 non-null int64 16 mjd 310000 non-null int64 17 fiberid 310000 non-null int64 dtypes: float64(8), int64(7), object(3) memory usage: 42.6+ MB
Usuwamy niepotrzebne dane takie jak ID obserwacji i inne techniczne informacje My będziemy korzystać z:
- u = Dane z filtra ultrafioletu
- g = Dane z filtra zielonego
- r = Dane z filtra czerwonego
- i = Dane z filtra bliskiej podczerwi (Near Infrared)
- z = Dane z filtra podczerwieni
- redshift = przesunięcie ku czerwieni na podstawie zwiększenia długości fali
Jasności na filtrach są podawane w Asinh magnitudo. Wyraża się ono wzorem:
\begin{aligned} \frac{-2.5}{\ln 10} * \text{arcsinh}( \frac{\frac{f}{f_0}}{2b} +\ln(b)) \end{aligned}
gdzie:
- b jest stałą zmiękczającą można sprawdzić pod linkiem.
- f$_0$ to jasność odniesienia. Klasycznie jest to jasność gwiazdy Vega.
- f to obserwowana jasność
df=df.drop(columns=['objid','run','rerun','camcol','field','specobjid','plate','mjd','fiberid'])
Dodatkowo pozbędziemy się fałszywych danych poza zakresami
df.describe()
| ra | dec | u | g | r | i | z | redshift | |
|---|---|---|---|---|---|---|---|---|
| count | 310000.000000 | 310000.000000 | 310000.000000 | 310000.000000 | 310000.000000 | 310000.000000 | 310000.000000 | 310000.000000 |
| mean | 174.694084 | 22.097973 | 19.716598 | 18.413805 | 17.770995 | 17.382543 | 17.075167 | 0.300031 |
| std | 85.593731 | 22.738541 | 18.124083 | 18.109926 | 1.907456 | 18.081449 | 36.024479 | 0.579806 |
| min | 0.003092 | -19.495456 | -9999.000000 | -9999.000000 | 9.005167 | -9999.000000 | -9999.000000 | -0.009971 |
| 25% | 131.354822 | 1.042457 | 18.510617 | 17.143935 | 16.472307 | 16.136660 | 15.908515 | 0.000107 |
| 50% | 175.860912 | 17.145758 | 19.184505 | 17.904820 | 17.357705 | 17.063675 | 16.893270 | 0.070177 |
| 75% | 223.633481 | 41.176901 | 20.162707 | 19.213520 | 18.882188 | 18.631750 | 18.436143 | 0.279405 |
| max | 359.999810 | 84.490494 | 32.781390 | 31.602240 | 31.990100 | 32.141470 | 29.383740 | 7.011245 |
df['class'].describe()
count 310000 unique 3 top GALAXY freq 168109 Name: class, dtype: object
for column in ['ra','dec','class']: # Ensure numerical data
plt.figure() # Create a new figure for each histogram
sns.histplot(df[column], kde=True, bins=100, color='blue') # Histogram with KDE
plt.title(f'Histogram for {column}')
plt.xlabel(column)
plt.ylabel('Amount')
plt.grid(axis='y', alpha=0.75)
plt.show()
plt.figure() # Create a new figure for each histogram
for column in df.select_dtypes(include=['number']).columns:
if column != 'ra'and column!='dec' and column != 'class':
sns.histplot(data=df,x=column,label=column,multiple='stack', kde=False, bins=10) # Histogram with KDE
plt.title(f'Histogram for our data')
plt.legend()
plt.xlabel('value')
plt.ylabel('amount')
plt.grid(axis='y', alpha=0.75)
plt.show()
Wybieramy zakresy dla poszczególnych kolumn a następnie filtrujemy dane - ryzykujemy utratę niektórych przypadków, ale ewidentnie w zakresie danych są takie które są fizycznie niemożliwe:
ranges={
"u": (-2000,50),
"g": (-2000,50),
"r": (0,50),
"z": (-2000,50),
}
for column, (min_val, max_val) in ranges.items():
df = df[(df[column] >= min_val) & (df[column] <= max_val)]
df = df.reset_index(drop=True)
plt.figure() # Create a new figure for each histogram
for column in df.select_dtypes(include=['number']).columns:
if column != 'ra'and column!='dec' and column != 'class':
sns.histplot(data=df,x=column,label=column,multiple='stack', kde=False, bins=10) # Histogram with KDE
plt.title(f'Histogram for our data')
plt.legend()
plt.xlabel('value')
plt.ylabel('amount')
plt.grid(axis='y', alpha=0.75)
plt.show()
Podzielmy dane na dwa oddzielne zestawy:
y=df['class']
x=df.drop('class',axis=1)
numeric_df = x.select_dtypes(include=[np.number])
scaler = StandardScaler()
scaled_data = scaler.fit_transform(numeric_df)
Teraz przejdziemy do właściwej analizy sprawdzimy dwie rzeczy:
- jak liczba składowych wpływa na całkowitą wariancję
- następnie wybierzemy jedną konkretną liczbę składowych głownych i obliczymy składowe główne dla niej
# Tworzenie obiektu PCA, gdzie nie określasz liczby komponentów
pca = PCA()
# Dopasowanie i transformacja danych
pca.fit(scaled_data)
# Sprawdzenie skumulowanej wariancji wyjaśnianej przez kolejne komponenty
cumulative_variance = np.cumsum(pca.explained_variance_ratio_)
print(f"Skumulowana wariancja: {cumulative_variance}")
# Wykres skumulowanej wariancji
plt.figure(figsize=(8,5))
plt.plot(cumulative_variance)
plt.xlabel('Liczba głównych składowych')
plt.ylabel('Skumulowana wyjaśniona wariancja')
plt.title('Wykres skumulowanej wyjaśnionej wariancji')
plt.grid(True)
plt.show()
Skumulowana wariancja: [0.61301954 0.74765284 0.86279967 0.95484645 0.99312401 0.99754811 0.99902642 1. ]
# Utworzenie obiektu PCA
n_components=4 #liczba składowych głównych
pca = PCA(n_components)
# Dopasowanie i transformacja danych
principal_components = pca.fit_transform(scaled_data)
# Sprawdzenie ile wariancji wyjaśniają komponenty
explained_variance = pca.explained_variance_ratio_
print(f"Wariancja wyjaśniana przez każdą główną składową: {explained_variance}")
# Utworzenie DataFrame z wynikami analizy PCA (ze zredukowanymi wymiarami)
pca_df = pd.DataFrame(data=principal_components, columns=[f'PC{i+1}' for i in range(n_components)])
# Uzyskanie macierzy składowych
components = pca.components_
# Tworzenie DataFrame z wynikami
components_df = pd.DataFrame(components, columns=x.select_dtypes(include=[np.number]).columns, index=[f'PC{i+1}' for i in range(components.shape[0])])
# Wyświetlenie macierzy składowych
print(components_df)
Wariancja wyjaśniana przez każdą główną składową: [0.61301954 0.1346333 0.11514683 0.09204678]
ra dec u g r i z \
PC1 0.009132 0.029960 0.393228 0.437499 0.446836 0.440635 0.430594
PC2 0.712033 0.701017 -0.016387 -0.014054 -0.015194 -0.015238 -0.015914
PC3 -0.700426 0.707398 -0.050002 -0.026158 -0.011492 -0.002054 0.004495
PC4 0.045491 -0.085055 -0.357515 -0.198924 -0.076150 0.001872 0.041001
redshift
PC1 0.271574
PC2 0.020044
PC3 0.075169
PC4 0.903230
Widzimy, że wartość dla ra i dec w pierwszej składowej są bardzo małe, natomiast składowe które je wyjaśniają (2 i 3) bardzo słabo wyjaśniają pozostałe. Sprawdźmy na wszelki wypadek macierz korelacji, bo być może ra i dec są wgl niepotrzebne.
plt.figure(figsize=(10,6))
correlation = pd.concat([x,y],axis=1)
sns.heatmap(x.corr(), annot=True,cmap='jet',fmt='.2f') #jet,copper, coolwarm
plt.title('Correlation matrix')
plt.show()
Wartości ra i dec praktycznie wgl nie pomagają nam w klasyfikacji, pozbędziemy się ich i jeszcz raz ustalimy składowe główne.
x=x.loc[:, x.columns != 'ra']
x=x.loc[:, x.columns != 'dec']
numeric_df = x.select_dtypes(include=[np.number])
scaler = StandardScaler()
scaled_data = scaler.fit_transform(numeric_df)
# Tworzenie obiektu PCA, gdzie nie określasz liczby komponentów
pca = PCA()
# Dopasowanie i transformacja danych
pca.fit(scaled_data)
# Sprawdzenie skumulowanej wariancji wyjaśnianej przez kolejne komponenty
cumulative_variance = np.cumsum(pca.explained_variance_ratio_)
print(f"Skumulowana wariancja: {cumulative_variance}")
# Wykres skumulowanej wariancji
plt.figure(figsize=(8,5))
plt.plot(cumulative_variance)
plt.xlabel('Liczba głównych składowych')
plt.ylabel('Skumulowana wyjaśniona wariancja')
plt.title('Wykres skumulowanej wyjaśnionej wariancji')
plt.grid(True)
plt.show()
Skumulowana wariancja: [0.81672772 0.93976303 0.99083011 0.99673048 0.99870185 1. ]
# Utworzenie obiektu PCA
n_components=2 #liczba składowych głównych
pca = PCA(n_components)
# Dopasowanie i transformacja danych
principal_components = pca.fit_transform(scaled_data)
# Sprawdzenie ile wariancji wyjaśniają komponenty
explained_variance = pca.explained_variance_ratio_
print(f"Wariancja wyjaśniana przez każdą główną składową: {explained_variance}")
# Utworzenie DataFrame z wynikami analizy PCA (ze zredukowanymi wymiarami)
pca_df = pd.DataFrame(data=principal_components, columns=[f'PC{i+1}' for i in range(n_components)])
# Uzyskanie macierzy składowych
components = pca.components_
# Tworzenie DataFrame z wynikami
components_df = pd.DataFrame(components, columns=x.select_dtypes(include=[np.number]).columns, index=[f'PC{i+1}' for i in range(components.shape[0])])
# Wyświetlenie macierzy składowych
print(components_df)
Wariancja wyjaśniana przez każdą główną składową: [0.81672772 0.12303531]
u g r i z redshift
PC1 0.393470 0.437721 0.447063 0.440858 0.430811 0.271593
PC2 -0.361179 -0.200761 -0.077152 0.001429 0.041021 0.906426
PCA_values = x.values @ components_df.T.values
df_PCA_x = pd.DataFrame(PCA_values,columns=[f'PC{i+1}' for i in range(components.shape[0])])
#df_PCA=pd.concat([df_PCA_x,y],axis=1)
df_PCA = df_PCA_x
df_PCA['class'] = pd.DataFrame(y)
df_PCA
df_PCA
| PC1 | PC2 | class | |
|---|---|---|---|
| 0 | 35.645608 | -11.038692 | STAR |
| 1 | 36.664800 | -10.787298 | STAR |
| 2 | 38.206279 | -11.175851 | GALAXY |
| 3 | 35.380231 | -10.321350 | STAR |
| 4 | 35.829161 | -10.166418 | STAR |
| ... | ... | ... | ... |
| 309991 | 39.278134 | -11.324122 | STAR |
| 309992 | 38.765651 | -11.136801 | STAR |
| 309993 | 36.659873 | -10.908642 | GALAXY |
| 309994 | 35.120367 | -10.100886 | STAR |
| 309995 | 36.016473 | -10.425274 | GALAXY |
309996 rows × 3 columns
sns.scatterplot(x='PC1',y='PC2',hue='class',data=df_PCA)
<Axes: xlabel='PC1', ylabel='PC2'>
Alternatywnie moglibyśmy również zmaksymalizować dokładność poprzez dodanie jeszcze jednej składowej
# Utworzenie obiektu PCA
n_components=3 #liczba składowych głównych
pca = PCA(n_components)
# Dopasowanie i transformacja danych
principal_components = pca.fit_transform(scaled_data)
# Sprawdzenie ile wariancji wyjaśniają komponenty
explained_variance = pca.explained_variance_ratio_
print(f"Wariancja wyjaśniana przez każdą główną składową: {explained_variance}")
# Utworzenie DataFrame z wynikami analizy PCA (ze zredukowanymi wymiarami)
pca_df = pd.DataFrame(data=principal_components, columns=[f'PC{i+1}' for i in range(n_components)])
# Uzyskanie macierzy składowych
components = pca.components_
# Tworzenie DataFrame z wynikami
components_df = pd.DataFrame(components, columns=x.select_dtypes(include=[np.number]).columns, index=[f'PC{i+1}' for i in range(components.shape[0])])
# Wyświetlenie macierzy składowych
print(components_df)
Wariancja wyjaśniana przez każdą główną składową: [0.81672772 0.12303531 0.05106709]
u g r i z redshift
PC1 0.393470 0.437721 0.447063 0.440858 0.430811 0.271593
PC2 -0.361179 -0.200761 -0.077152 0.001429 0.041021 0.906426
PC3 0.665579 0.212693 -0.146436 -0.363845 -0.503462 0.323212
PCA_values = x.values @ components_df.T.values
df_PCA_x = pd.DataFrame(PCA_values,columns=[f'PC{i+1}' for i in range(components.shape[0])])
#df_PCA=pd.concat([df_PCA_x,y],axis=1)
df_PCA_alternative = df_PCA_x
df_PCA_alternative['class'] = pd.DataFrame(y)
df_PCA_alternative
| PC1 | PC2 | PC3 | class | |
|---|---|---|---|---|
| 0 | 35.645608 | -11.038692 | 0.944882 | STAR |
| 1 | 36.664800 | -10.787298 | -0.611106 | STAR |
| 2 | 38.206279 | -11.175851 | -0.424870 | GALAXY |
| 3 | 35.380231 | -10.321350 | -0.833439 | STAR |
| 4 | 35.829161 | -10.166418 | -1.654420 | STAR |
| ... | ... | ... | ... | ... |
| 309991 | 39.278134 | -11.324122 | -1.299621 | STAR |
| 309992 | 38.765651 | -11.136801 | -1.396056 | STAR |
| 309993 | 36.659873 | -10.908642 | 0.104838 | GALAXY |
| 309994 | 35.120367 | -10.100886 | -1.231751 | STAR |
| 309995 | 36.016473 | -10.425274 | -1.029245 | GALAXY |
309996 rows × 4 columns
Wyświetlimy wykres interaktywnie, w celu poprawy wydajności pobierzemy więc próbkę naszych punktów (~1/3 całości)
import plotly.express as px
df_PCA_alternative_sample=df_PCA_alternative.sample(100000, random_state=32)
fig=px.scatter_3d(df_PCA_alternative_sample,x='PC1',y='PC2',z='PC3',color='class' )
fig.update_traces(marker={'size': 1})
fig.show()
Dodanie dodatkowego wymiaru uwypukla poprzednio już widoczną różnicę między QSO a innymi obiektami. Dodatkowo nieco wyraźniejsza staje się różnica między gwiazdami i galaktymi - te drugie przyjmują bliższe są ujemnych wartości dla PC3.
Klasteryzacja
KMeans
Normalizacja danych - przygotowanie do klasteryzacji
pd.set_option('future.no_silent_downcasting', True)
uniques = pd.unique(df['class'])
binary_map = {}
for number, value in enumerate(uniques):
binary_map[value] = number
df['class'] = df['class'].replace(binary_map)
binary_map
{'STAR': 0, 'GALAXY': 1, 'QSO': 2}
df_class = df['class']
df = df.drop('class',axis=1)
scaler = StandardScaler()
data_scaled = scaler.fit_transform(df)
df['class'] = df_class
Przed normalizacją usuneliśmy z danych klase obiektu żeby nie mała ona wpływu na dopasowanie do klasteru
Klasteryzacja z użyciem KMeans
kmeans = KMeans(n_clusters=3, random_state=42)
clusters = kmeans.fit_predict(data_scaled)
df['Cluster'] = clusters
Metoda łokcia
Metoda łokcia to technika stosowana w analizie skupień (clustering) w celu określenia optymalnej liczby klastrów w algorytmie grupowania, takim jak k-means
inertias = []
for k in range(1, 11):
kmeans_ml = KMeans(n_clusters=k, random_state=42)
kmeans_ml.fit(data_scaled)
inertias.append(kmeans_ml.inertia_)
plt.plot(range(1, 11), inertias, marker='o')
plt.title("Metoda Łokcia")
plt.xlabel("Liczba klastrów")
plt.ylabel("Inercja")
plt.show()
W naszym przypadku zdecydowaliśmy się na 3 klastry
Rozkład danych
df.groupby('Cluster').mean()
| ra | dec | u | g | r | i | z | redshift | class | |
|---|---|---|---|---|---|---|---|---|---|
| Cluster | |||||||||
| 0 | 176.872015 | 24.617398 | 22.842321 | 21.575674 | 20.598721 | 19.990958 | 19.663840 | 0.892420 | 1.196258 |
| 1 | 170.730654 | 19.173954 | 18.199397 | 16.663351 | 15.988618 | 15.676159 | 15.462196 | 0.044983 | 0.618286 |
| 2 | 176.855472 | 23.161519 | 19.287995 | 18.165989 | 17.666329 | 17.418949 | 17.279495 | 0.175697 | 0.740211 |
W powyższej tabeli widzimy srednią wartość poszczególnych kolumn dla każdego z klastrów Poniżej znajduje liczba elementów przypadających na dany klaster i klase
df['Cluster'].value_counts()
Cluster 2 126641 1 109590 0 73765 Name: count, dtype: int64
df['class'].value_counts()
class 1 168107 0 101072 2 40817 Name: count, dtype: int64
plt.figure(figsize=(6, 5))
sns.heatmap(pd.crosstab(df['class'], df['Cluster']), annot=True, linewidths=0.5, fmt='d')
plt.title("Contingency Table Heatmap: Class vs Cluster")
plt.ylabel("Class")
plt.xlabel("Cluster")
plt.show()
Reprezentacja ilości elementów danej klasy w danym klastrze
contingency_table = pd.crosstab(df['class'], df['Cluster'])
contingency_percentage = contingency_table.div(contingency_table.sum(axis=1), axis=0) * 100
plt.figure(figsize=(6, 5))
sns.heatmap(contingency_percentage, annot=True, linewidths=0.5, fmt='f')
plt.title("Contingency Table Heatmap: Class vs Cluster")
plt.ylabel("Class")
plt.xlabel("Cluster")
plt.show()
Reprezentacja rozkładu elementów klas na klastry
Poniżej znajduje sie kilka wykresów przedstawiających wizualizacje podziału
feature_1 = df['z']
feature_2 = df['g']
feature_3 = df['u']
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(feature_1, feature_2, feature_3, c=clusters, cmap='viridis', s=1, alpha=0.2)
ax.set_title("Wizualizacja klasteryzacji w 3D")
ax.set_xlabel("Cecha 1")
ax.set_ylabel("Cecha 2")
ax.set_zlabel("Cecha 3")
ax.view_init(elev=40, azim=240)
ax.grid(True)
plt.show()
feature_1 = df['ra']
feature_2 = df['dec']
feature_3 = df['u']
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(feature_1, feature_2, feature_3, c=clusters, cmap='viridis', s=1, alpha=0.2)
ax.set_title("Wizualizacja klasteryzacji w 3D")
ax.set_xlabel("Cecha 1")
ax.set_ylabel("Cecha 2")
ax.set_zlabel("Cecha 3")
ax.view_init(elev=40, azim=240)
ax.grid(True)
plt.show()
feature_1 = df['redshift']
feature_2 = df['class']
feature_3 = df['u']
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(feature_1, feature_2, feature_3, c=clusters, cmap='viridis', s=1, alpha=0.2)
ax.set_title("Wizualizacja klasteryzacji w 3D")
ax.set_xlabel("Cecha 1")
ax.set_ylabel("Cecha 2")
ax.set_zlabel("Cecha 3")
ax.view_init(elev=40, azim=240)
ax.grid(True)
plt.show()
KMeans na PCA
Dla prorównania przeprowadzamy klsateryzacje na danych które zostały poddane redukcji wymiarów
uniques = pd.unique(df_PCA['class'])
binary_map = {}
for number, value in enumerate(uniques):
binary_map[value] = number
df_PCA['class'] = df_PCA['class'].replace(binary_map)
binary_map
{'STAR': 0, 'GALAXY': 1, 'QSO': 2}
scaler_pca = StandardScaler()
df_PCA_class = df_PCA['class']
df_PCA = df_PCA.drop('class',axis=1)
data_scaled_pca = scaler.fit_transform(df_PCA)
df_PCA['class'] = df_PCA_class
kmeans_pca = KMeans(n_clusters=3, random_state=42)
clusters_pca = kmeans_pca.fit_predict(data_scaled_pca)
df_PCA['Cluster'] = clusters_pca
inertias = []
for k in range(1, 11):
kmeans_ml = KMeans(n_clusters=k, random_state=42)
kmeans_ml.fit(data_scaled_pca)
inertias.append(kmeans_ml.inertia_)
plt.plot(range(1, 11), inertias, marker='o')
plt.title("Metoda Łokcia")
plt.xlabel("Liczba klastrów")
plt.ylabel("Inercja")
plt.show()
df_PCA.groupby('Cluster').mean()
| PC1 | PC2 | class | |
|---|---|---|---|
| Cluster | |||
| 0 | 45.512034 | -13.098932 | 0.974886 |
| 1 | 38.619602 | -10.988799 | 0.854203 |
| 2 | 34.834026 | -10.239338 | 0.587558 |
df_PCA['Cluster'].value_counts()
Cluster 1 163927 2 84390 0 61679 Name: count, dtype: int64
df_PCA['class'].value_counts()
class 1 168107 0 101072 2 40817 Name: count, dtype: int64
plt.figure(figsize=(6, 5))
sns.heatmap(pd.crosstab(df_PCA['class'], df_PCA['Cluster']), annot=True, linewidths=0.5, fmt='d')
plt.title("Contingency Table Heatmap: Class vs Cluster")
plt.ylabel("Class")
plt.xlabel("Cluster")
plt.show()
contingency_table = pd.crosstab(df_PCA['class'], df_PCA['Cluster'])
contingency_percentage = contingency_table.div(contingency_table.sum(axis=1), axis=0) * 100
plt.figure(figsize=(6, 5))
sns.heatmap(contingency_percentage, annot=True, linewidths=0.5, fmt='f')
plt.title("Contingency Table Heatmap: Class vs Cluster")
plt.ylabel("Class")
plt.xlabel("Cluster")
plt.show()
feature_1 = df_PCA['PC1']
feature_2 = df_PCA['PC2']
feature_3 = df_PCA['class'].astype(float)
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(feature_1, feature_2, feature_3, c=clusters, cmap='viridis', s=1, alpha=0.2)
ax.set_title("Wizualizacja klasteryzacji w 3D")
ax.set_xlabel("Cecha 1")
ax.set_ylabel("Cecha 2")
ax.set_zlabel("Cecha 3")
ax.view_init(elev=40, azim=240)
ax.grid(True)
plt.show()
Analiza koszykowa/reguły asocjacyjne
odnawiamy opis klas z liczb na słowa
df['class'] = pd.DataFrame(y)
Spójrzmy na zakresy w jakich znajdują się nasze dane, aby uzyskać intuicyjne pojęcie na temat tego, czego będziemy szukać.
def calculate_ranges(df, features, target):
rules = {}
for cls in df[target].unique():
class_data = df[df[target] == cls]
rules[cls] = {}
for feature in features:
rules[cls][feature] = {
"min": class_data[feature].min(),
"max": class_data[feature].max()
}
return rules
features = ["u", "g", "r","z","i","redshift"]
rules = calculate_ranges(df, features, target="class")
for cls, cls_rules in rules.items():
print(f"Zakresy dla klasy '{cls}':")
for feature, bounds in cls_rules.items():
print(f" {feature}: {bounds['min']} <= x <= {bounds['max']}")
Zakresy dla klasy 'STAR': u: 10.61181 <= x <= 30.66039 g: 9.668339 <= x <= 30.607 r: 9.005167 <= x <= 31.69816 z: 8.947795 <= x <= 28.6687 i: 8.848403 <= x <= 30.98087 redshift: -0.004267853 <= x <= 0.004563184 Zakresy dla klasy 'GALAXY': u: 11.72647 <= x <= 29.32565 g: 11.85493 <= x <= 31.60224 r: 10.8467 <= x <= 31.9901 z: 10.42036 <= x <= 29.38374 i: 10.5178 <= x <= 32.10178 redshift: -0.009970667 <= x <= 1.995524 Zakresy dla klasy 'QSO': u: 10.99623 <= x <= 32.78139 g: 10.70823 <= x <= 27.89482 r: 9.801497 <= x <= 28.37412 z: 10.06144 <= x <= 28.79055 i: 9.557886 <= x <= 32.14147 redshift: 0.000460623 <= x <= 7.011245
Aby przeprowadzić analizę koszykową musimy przejść ze zmiennych ciągłych na dyskretne. Wyznaczymy odpowiednie zakresy. Jak wspomniano poprzednio wartości u, g, r, z, i to zasadniczo jasności w różnych spektrach światła, dlatego podzielimy je na identyczne zakresy:
- $\infty > x > 22$, blade (dim)
- $20 >= x > 17$, średnie (medium)
- $17 >= x > - \infty $, jasne (bright)
W przypadku przesunięcia ku czerwieni wybierzemy wartości:
- $x<0.05 $, niskie
- $0.05<=x<1.5$, średnie
- $ 2<=x$, wysokie
Wybór zakresów jest orientacyjny, ich większa ilość zapewne zwiększyłaby dokładność.
x_copy = x.copy()
medium_upperbound = 22
bright_upperbound = 17
low_redshift_upperbound = 0.05
medium_redshift_upperbound = 1.5
for column in x_copy:
if column!="redshift":
name = "dim_"+column
x_copy[name] = x_copy[column] > medium_upperbound
name = "medium_"+column
x_copy[name] = (x_copy[column] <= medium_upperbound) & (x_copy[column] > bright_upperbound)
name= "bright_"+column
x_copy[name] =x_copy[column] <= bright_upperbound
else:
name="low_"+column
x_copy[name]=x_copy[column]<low_redshift_upperbound
name="medium_"+column
x_copy[name]=(x_copy[column]>=low_redshift_upperbound) & (x_copy[column]<medium_redshift_upperbound)
name="high_"+column
x_copy[name]=x_copy[column]>=medium_redshift_upperbound
x_copy
| u | g | r | i | z | redshift | dim_u | medium_u | bright_u | dim_g | ... | bright_r | dim_i | medium_i | bright_i | dim_z | medium_z | bright_z | low_redshift | medium_redshift | high_redshift | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 19.47406 | 17.04240 | 15.94699 | 15.50342 | 15.22531 | -0.000009 | False | True | False | False | ... | True | False | False | True | False | False | True | True | False | False |
| 1 | 18.66280 | 17.21449 | 16.67637 | 16.48922 | 16.39150 | -0.000055 | False | True | False | False | ... | True | False | False | True | False | False | True | True | False | False |
| 2 | 19.38298 | 18.19169 | 17.47428 | 17.08732 | 16.80125 | 0.123111 | False | True | False | False | ... | False | False | True | False | False | False | True | False | True | False |
| 3 | 17.76536 | 16.60272 | 16.16116 | 15.98233 | 15.90438 | -0.000111 | False | True | False | False | ... | True | False | False | True | False | False | True | True | False | False |
| 4 | 17.55025 | 16.26342 | 16.43869 | 16.55492 | 16.61326 | 0.000590 | False | True | False | False | ... | True | False | False | True | False | False | True | True | False | False |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 309991 | 19.39861 | 18.35476 | 18.00348 | 17.89408 | 17.81222 | -0.000101 | False | True | False | False | ... | False | False | True | False | False | True | False | True | False | False |
| 309992 | 19.07703 | 18.05159 | 17.78332 | 17.68976 | 17.66209 | -0.000352 | False | True | False | False | ... | False | False | True | False | False | True | False | True | False | False |
| 309993 | 19.07982 | 17.51349 | 16.64037 | 16.24183 | 15.91180 | 0.117501 | False | True | False | False | ... | True | False | False | True | False | False | True | False | True | False |
| 309994 | 17.27528 | 16.41704 | 16.11662 | 15.98858 | 15.97745 | -0.000400 | False | True | False | False | ... | True | False | False | True | False | False | True | True | False | False |
| 309995 | 17.90598 | 16.86471 | 16.51673 | 16.35695 | 16.22508 | 0.014457 | False | True | False | False | ... | True | False | False | True | False | False | True | True | False | False |
309996 rows × 24 columns
Zmapujemy również rodzaj naszego obiektu.
df_encoded = df.copy()
x_copy['class'] = pd.DataFrame(y)
x_copy.drop(features,axis=1,inplace=True)
x_copy['is_Star']=x_copy['class']=='STAR'
x_copy['is_Galaxy']=x_copy['class']=='GALAXY'
x_copy['is_QSO']=x_copy['class']=='QSO'
x_copy.drop('class',axis=1,inplace=True)
Następnie odnajdziemy reguły wskazujące na rodzaj naszego obiektu.
from mlxtend.frequent_patterns import apriori, association_rules
# Używamy apriori do znalezienia częstych zbiorów
frequent_itemsets = apriori(x_copy, min_support=0.05, use_colnames=True)
# Generowanie reguł asocjacyjnych
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.05,num_itemsets=len(x_copy))
# Filtrujemy reguły, które odnoszą się do kolumny 'class'
rules['consequents'] = rules['consequents'].apply(lambda x: list(x)[0]) # Zamieniamy krotki na elementy
star_rules=rules[rules['consequents']=='is_Star']
galaxy_rules =rules[rules['consequents']=='is_Galaxy']
qso_rules =rules[rules['consequents']=='is_QSO']
star_rules
| antecedents | consequents | antecedent support | consequent support | support | confidence | lift | representativity | leverage | conviction | zhangs_metric | jaccard | certainty | kulczynski | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 34 | (medium_u) | is_Star | 0.790971 | 0.326043 | 0.276097 | 0.349061 | 1.070598 | 1.0 | 0.018206 | 1.035361 | 0.315470 | 0.328328 | 0.034153 | 0.597936 |
| 68 | (medium_g) | is_Star | 0.694667 | 0.326043 | 0.206370 | 0.297078 | 0.911163 | 1.0 | -0.020121 | 0.958794 | -0.242033 | 0.253421 | -0.042977 | 0.465016 |
| 84 | (bright_g) | is_Star | 0.214100 | 0.326043 | 0.111150 | 0.519150 | 1.592276 | 1.0 | 0.041344 | 1.401596 | 0.473302 | 0.259095 | 0.286528 | 0.430028 |
| 102 | (medium_r) | is_Star | 0.584391 | 0.326043 | 0.168592 | 0.288492 | 0.884830 | 1.0 | -0.021944 | 0.947224 | -0.238491 | 0.227262 | -0.055716 | 0.402790 |
| 116 | (bright_r) | is_Star | 0.396483 | 0.326043 | 0.154854 | 0.390569 | 1.197905 | 1.0 | 0.025583 | 1.105878 | 0.273744 | 0.272787 | 0.095741 | 0.432759 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 15044 | (bright_i, bright_g) | is_Star | 0.212190 | 0.130382 | 0.085766 | 0.404193 | 3.100059 | 1.0 | 0.058100 | 1.459562 | 0.859884 | 0.333970 | 0.314863 | 0.530997 |
| 15045 | (bright_i, bright_r) | is_Star | 0.395179 | 0.086040 | 0.085766 | 0.217030 | 2.522433 | 1.0 | 0.051764 | 1.167299 | 0.997911 | 0.216879 | 0.143321 | 0.606921 |
| 15046 | (bright_i, medium_u) | is_Star | 0.444712 | 0.109066 | 0.085766 | 0.192856 | 1.768256 | 1.0 | 0.037263 | 1.103811 | 0.782425 | 0.183255 | 0.094048 | 0.489611 |
| 15047 | (bright_i, bright_z) | is_Star | 0.480429 | 0.086546 | 0.085766 | 0.178519 | 2.062699 | 1.0 | 0.044186 | 1.111960 | 0.991584 | 0.178229 | 0.100687 | 0.584749 |
| 15063 | (bright_i) | is_Star | 0.481693 | 0.085920 | 0.085766 | 0.178050 | 2.072268 | 1.0 | 0.044378 | 1.112087 | 0.998322 | 0.177993 | 0.100790 | 0.588124 |
237 rows × 14 columns
galaxy_rules
| antecedents | consequents | antecedent support | consequent support | support | confidence | lift | representativity | leverage | conviction | zhangs_metric | jaccard | certainty | kulczynski | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13 | (dim_u) | is_Galaxy | 0.171425 | 0.542288 | 0.125508 | 0.732147 | 1.350107 | 1.0 | 0.032547 | 1.708816 | 0.312969 | 0.213375 | 0.414800 | 0.481794 |
| 36 | (medium_u) | is_Galaxy | 0.790971 | 0.542288 | 0.404347 | 0.511203 | 0.942679 | 1.0 | -0.024587 | 0.936406 | -0.225347 | 0.435291 | -0.067913 | 0.628418 |
| 49 | (dim_g) | is_Galaxy | 0.091233 | 0.542288 | 0.074553 | 0.817163 | 1.506881 | 1.0 | 0.025078 | 2.503388 | 0.370147 | 0.133375 | 0.600541 | 0.477320 |
| 70 | (medium_g) | is_Galaxy | 0.694667 | 0.542288 | 0.366821 | 0.528053 | 0.973750 | 1.0 | -0.009888 | 0.969838 | -0.081126 | 0.421568 | -0.031100 | 0.602243 |
| 86 | (bright_g) | is_Galaxy | 0.214100 | 0.542288 | 0.100914 | 0.471342 | 0.869174 | 1.0 | -0.015189 | 0.865801 | -0.160737 | 0.153956 | -0.154999 | 0.328716 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 14825 | (bright_i, medium_redshift, medium_g, bright_r... | is_Galaxy | 0.118827 | 0.343411 | 0.117302 | 0.987159 | 2.874572 | 1.0 | 0.076495 | 51.133439 | 0.740062 | 0.340067 | 0.980443 | 0.664369 |
| 14829 | (bright_i, medium_redshift, medium_g, medium_u... | is_Galaxy | 0.179244 | 0.237935 | 0.117302 | 0.654423 | 2.750423 | 1.0 | 0.074653 | 2.205193 | 0.775407 | 0.391164 | 0.546525 | 0.573710 |
| 15071 | (bright_i, medium_redshift, bright_g, bright_r... | is_Galaxy | 0.051030 | 0.542288 | 0.050110 | 0.981984 | 1.810817 | 1.0 | 0.022438 | 25.405445 | 0.471841 | 0.092249 | 0.960638 | 0.537195 |
| 15077 | (bright_i, medium_redshift, bright_g, bright_r... | is_Galaxy | 0.051033 | 0.343411 | 0.050110 | 0.981922 | 2.859320 | 1.0 | 0.032585 | 36.319020 | 0.685236 | 0.145528 | 0.972466 | 0.563921 |
| 15081 | (bright_i, medium_redshift, bright_g, medium_u... | is_Galaxy | 0.051036 | 0.237935 | 0.050110 | 0.981860 | 4.126582 | 1.0 | 0.037967 | 42.009147 | 0.798417 | 0.209789 | 0.976196 | 0.596232 |
558 rows × 14 columns
qso_rules
| antecedents | consequents | antecedent support | consequent support | support | confidence | lift | representativity | leverage | conviction | zhangs_metric | jaccard | certainty | kulczynski | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 38 | (medium_u) | is_QSO | 0.790971 | 0.131669 | 0.110527 | 0.139736 | 1.061264 | 1.0 | 0.006380 | 1.009377 | 0.276170 | 0.136098 | 0.009290 | 0.489583 |
| 72 | (medium_g) | is_QSO | 0.694667 | 0.131669 | 0.121476 | 0.174869 | 1.328091 | 1.0 | 0.030009 | 1.052355 | 0.809083 | 0.172340 | 0.049750 | 0.548725 |
| 106 | (medium_r) | is_QSO | 0.584391 | 0.131669 | 0.124779 | 0.213520 | 1.621634 | 1.0 | 0.047833 | 1.104071 | 0.922353 | 0.211031 | 0.094262 | 0.580594 |
| 132 | (medium_i) | is_QSO | 0.512784 | 0.131669 | 0.124666 | 0.243116 | 1.846414 | 1.0 | 0.057148 | 1.147244 | 0.940875 | 0.239841 | 0.128346 | 0.594964 |
| 154 | (medium_z) | is_QSO | 0.465412 | 0.131669 | 0.123369 | 0.265075 | 2.013187 | 1.0 | 0.062089 | 1.181523 | 0.941427 | 0.260431 | 0.153635 | 0.601019 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 13196 | (medium_redshift, medium_z, medium_u, medium_i... | is_QSO | 0.116566 | 0.131669 | 0.059823 | 0.513214 | 3.897748 | 1.0 | 0.044475 | 1.783805 | 0.841536 | 0.317513 | 0.439401 | 0.483780 |
| 13692 | (medium_redshift, medium_z, medium_g, medium_i... | is_QSO | 0.169031 | 0.131669 | 0.062785 | 0.371438 | 2.820992 | 1.0 | 0.040528 | 1.381457 | 0.776822 | 0.263894 | 0.276126 | 0.424137 |
| 13754 | (medium_z, high_redshift, medium_g, medium_i, ... | is_QSO | 0.053007 | 0.131669 | 0.052917 | 0.998296 | 7.581835 | 1.0 | 0.045937 | 509.585991 | 0.916697 | 0.401616 | 0.998038 | 0.700094 |
| 14569 | (medium_redshift, medium_z, medium_g, medium_u... | is_QSO | 0.115395 | 0.131669 | 0.059520 | 0.515794 | 3.917344 | 1.0 | 0.044326 | 1.793310 | 0.841873 | 0.317366 | 0.442372 | 0.483918 |
| 14664 | (medium_redshift, medium_z) | is_QSO | 0.244571 | 0.105324 | 0.059520 | 0.243366 | 2.310638 | 1.0 | 0.033761 | 1.182442 | 0.750857 | 0.204977 | 0.154292 | 0.404240 |
82 rows × 14 columns
Dla każdego zestawu odnajdziemy najwyższe możliwe wartości pewności ('confidence') - które powie nam w jakiej części wypadków gdy wystąpi dany zestaw cech to analizowany obiekt jest danego typu.
print(star_rules.loc[star_rules['confidence'].idxmax()]['antecedents'])
star_rules['confidence'].max()
frozenset({'medium_i', 'medium_z', 'low_redshift'})
np.float64(0.8935126330843796)
print(galaxy_rules.loc[galaxy_rules['confidence'].idxmax()]['antecedents'])
galaxy_rules['confidence'].max()
frozenset({'bright_i', 'medium_redshift', 'medium_g', 'bright_r', 'bright_z'})
np.float64(0.9875383487633371)
print(qso_rules.loc[qso_rules['confidence'].idxmax()]['antecedents'])
qso_rules['confidence'].max()
frozenset({'medium_z', 'high_redshift', 'medium_g', 'medium_i', 'medium_r'})
np.float64(0.9982960077896786)
Porównując dane zauważyć możemy pewne charakterystyczne cechy np.
- Wyraźny rozdział widoczny jest w przesunięciu ku czerwieni. Gwiazdy znajdujące się najbliżej mają niską wartość, galaktyki średnią, a obiekty QSO wysoką. Ma to sens z fizycznego/astonomicznego punktu widzenia - obiekty znajdujące się dalej mają bowiem wyższa wartość przesunięcia.
- Galaktyki wskazywany są dodatkowo przez wysokie wartości światła w okolicy czerwieni/podczerwieni. Jest to interesująca obserwacja, galaktyki są często obserwowane w tej części spektrum, jako iż dobrze przenika on przez pył międzygwiezdny, ale jednocześniej nie jest do końca jasne dlaczego najjaśniejsze obiekty powinny być klasyfikowane jako galaktyki, a nie gwiazdy.
Nasze reguły okazały się najmniej skuteczne dla gwiazd, aby zrozumieć dlaczego posłużymy się jednak dalszą analizą - sprawdźmy jakie obiekty sa fałszywie wskazywane.
filtered_rows = x_copy[(x_copy['medium_z']) & (x_copy['low_redshift']) & (x_copy['medium_i']) & (~x_copy['is_Star']) ]
filtered_rows
| dim_u | medium_u | bright_u | dim_g | medium_g | bright_g | dim_r | medium_r | bright_r | dim_i | ... | bright_i | dim_z | medium_z | bright_z | low_redshift | medium_redshift | high_redshift | is_Star | is_Galaxy | is_QSO | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 9 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| 51 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| 76 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| 78 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| 121 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 309568 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| 309609 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| 309763 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| 309872 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
| 309942 | False | True | False | False | True | False | False | True | False | False | ... | False | False | True | False | True | False | False | False | True | False |
5361 rows × 21 columns
Wysnujemy hipotezę, iż fałszywa klasyfikacja wywołana jest tym, iż niektóre galaktyki znajdowały się na tyle blisko, iż zakwalifikowane zostały jako gwiazdy. Sprawdzimy to poprzez zmianę zakresów przesunięcia ku czerwieni na:
- $x<0.01 $, niskie
- $0.01<=x<1.5$, średnie
- $ 2<=x$, wysokie
I powtórzenie operacji
x_copy = x.copy()
medium_upperbound = 22
bright_upperbound = 17
low_redshift_upperbound = 0.01
medium_redshift_upperbound = 1.5
for column in x_copy:
if column!="redshift":
name = "dim_"+column
x_copy[name] = x_copy[column] > medium_upperbound
name = "medium_"+column
x_copy[name] = (x_copy[column] <= medium_upperbound) & (x_copy[column] > bright_upperbound)
name= "bright_"+column
x_copy[name] =x_copy[column] <= bright_upperbound
else:
name="low_"+column
x_copy[name]=x_copy[column]<low_redshift_upperbound
name="medium_"+column
x_copy[name]=(x_copy[column]>=low_redshift_upperbound) & (x_copy[column]<medium_redshift_upperbound)
name="high_"+column
x_copy[name]=x_copy[column]>=medium_redshift_upperbound
df_encoded = df.copy()
x_copy['class'] = pd.DataFrame(y)
x_copy.drop(features,axis=1,inplace=True)
x_copy['is_Star']=x_copy['class']=='STAR'
x_copy['is_Galaxy']=x_copy['class']=='GALAXY'
x_copy['is_QSO']=x_copy['class']=='QSO'
x_copy.drop('class',axis=1,inplace=True)
# Używamy apriori do znalezienia częstych zbiorów
frequent_itemsets = apriori(x_copy, min_support=0.05, use_colnames=True)
# Generowanie reguł asocjacyjnych
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.05,num_itemsets=len(x_copy))
# Filtrujemy reguły, które odnoszą się do kolumny 'class'
rules['consequents'] = rules['consequents'].apply(lambda x: list(x)[0]) # Zamieniamy krotki na elementy
star_rules=rules[rules['consequents']=='is_Star']
galaxy_rules =rules[rules['consequents']=='is_Galaxy']
qso_rules =rules[rules['consequents']=='is_QSO']
print(star_rules.loc[star_rules['confidence'].idxmax()]['antecedents'])
star_rules['confidence'].max()
frozenset({'medium_z', 'low_redshift', 'medium_g', 'medium_u', 'medium_i'})
np.float64(0.9839467708718381)
print(galaxy_rules.loc[galaxy_rules['confidence'].idxmax()]['antecedents'])
galaxy_rules['confidence'].max()
frozenset({'bright_i', 'medium_redshift', 'medium_g', 'bright_r', 'bright_z'})
np.float64(0.9890944909589813)
print(qso_rules.loc[qso_rules['confidence'].idxmax()]['antecedents'])
qso_rules['confidence'].max()
frozenset({'medium_z', 'high_redshift', 'medium_g', 'medium_i', 'medium_r'})
np.float64(0.9982960077896786)
Skuteczność naszej reguły w rozpoznawaniu gwiazd znacznie się zwiększyła - poprawa zakresów z pewnością zadziała. Wpływ przesunięcia ku czerwini na skuteczność reguł zdaje się być niezwykle wysoki. W dalszym ciągu galaktyki zdają się być jasniejsze w czerwieniach niż gwiazdy. Na koniec sprawdzimy odpowiedzi na dwa pytania wynikające z naszych obserwacji:
- Czy przesunięcie ku czerwieni wystarczy dla określania rodzaju obiektu
- Czy jasne w czerwieni obiekty to galaktyki?
Zacznijmy od przesunięcia ku czerwieni:
x_copy['class']=pd.DataFrame(y)
x_copy['low_redshift'] = x_copy['low_redshift'].astype(int)
x_copy['medium_redshift'] = x_copy['medium_redshift'].astype(int)
x_copy['high_redshift'] = x_copy['high_redshift'].astype(int)
x_copy.loc[x_copy['low_redshift'] == 1, 'redshift_category'] = 'Low'
x_copy.loc[x_copy['medium_redshift'] == 1, 'redshift_category'] = 'Medium'
x_copy.loc[x_copy['high_redshift'] == 1, 'redshift_category'] = 'High'
contingency_table = pd.crosstab(x_copy['class'], x_copy['redshift_category'])
contingency_percentage = contingency_table.div(contingency_table.sum(axis=1), axis=0) * 100
plt.figure(figsize=(8, 6))
sns.heatmap(contingency_percentage, annot=True, linewidths=0.5, fmt='f')
plt.title("Contingency Table Heatmap: Class vs Redshift Categories")
plt.ylabel("Class")
plt.xlabel("Redshift Categories")
plt.show()
Wskazać możemy, iż niskie przesunięcie ku czerwieni praktycznie jednoznacznie wksazuje na gwiazdy, a wysokie na QSO. Sprawa nie jest jednak oczywista dla średniego przesunięcia. Nie popełnilibyśmy dużego błędu mówiąc, że wskazuje ono na galaktykę, ale chcą odróżnić galaktyki od QSO wynik byłby znaczący, ta obserwacja jest jednak również pokrywająca się ze współczesną astronomią - QSO to zasadniczo odmiana galaktyk.
Przeanalizujmy teraz czerwienie:
x_copy['dim_r'] = x_copy['dim_r'].astype(int)
x_copy['medium_r'] = x_copy['medium_r'].astype(int)
x_copy['bright_r'] = x_copy['bright_r'].astype(int)
x_copy.loc[x_copy['dim_r'] == 1, 'r_category'] = 'Dim'
x_copy.loc[x_copy['medium_r'] == 1, 'r_category'] = 'Medium'
x_copy.loc[x_copy['bright_r'] == 1, 'r_category'] = 'Bright'
x_copy['dim_i'] = x_copy['dim_i'].astype(int)
x_copy['medium_i'] = x_copy['medium_i'].astype(int)
x_copy['bright_i'] = x_copy['bright_i'].astype(int)
x_copy.loc[x_copy['dim_i'] == 1, 'i_category'] = 'Dim'
x_copy.loc[x_copy['medium_i'] == 1, 'i_category'] = 'Medium'
x_copy.loc[x_copy['bright_i'] == 1, 'i_category'] = 'Bright'
x_copy['dim_z'] = x_copy['dim_z'].astype(int)
x_copy['medium_z'] = x_copy['medium_z'].astype(int)
x_copy['bright_z'] = x_copy['bright_z'].astype(int)
x_copy.loc[x_copy['dim_z'] == 1, 'z_category'] = 'Dim'
x_copy.loc[x_copy['medium_z'] == 1, 'z_category'] = 'Medium'
x_copy.loc[x_copy['bright_z'] == 1, 'z_category'] = 'Bright'
contingency_table_r = pd.crosstab(x_copy['class'], x_copy['r_category'])
contingency_percentage_r = contingency_table_r.div(contingency_table.sum(axis=1), axis=0) * 100
contingency_table_i = pd.crosstab(x_copy['class'], x_copy['i_category'])
contingency_percentage_i = contingency_table_i.div(contingency_table.sum(axis=1), axis=0) * 100
contingency_table_z = pd.crosstab(x_copy['class'], x_copy['z_category'])
contingency_percentage_z = contingency_table_z.div(contingency_table.sum(axis=1), axis=0) * 100
plt.figure(figsize=(12,10))
plt.subplot(2,2,1)
sns.heatmap(contingency_percentage_r, annot=True, linewidths=0.5, fmt='f')
plt.title("Class vs Red Light Categories")
plt.ylabel("Class")
plt.xlabel("Red Light Categories")
plt.subplot(2,2,2)
sns.heatmap(contingency_percentage_i, annot=True, linewidths=0.5, fmt='f')
plt.title("Class vs Near Infrared Categories")
plt.ylabel("Class")
plt.xlabel("Near Infrared Categories")
plt.subplot(2,2,3)
sns.heatmap(contingency_percentage_z, annot=True, linewidths=0.5, fmt='f')
plt.title("Class vs Infrared Categories")
plt.ylabel("Class")
plt.xlabel("Infrared Categories")
plt.show()
Zauważyć możemy, że obiekty o średniej jasności znajdują się praktycznie w każdej z grup, choć obiekty QSO najczęściej znajdują się w tej kategorii jasności, co znalazło odzwirciedlenie we wcześniej stworzonych regułach.
Obiekty jasne rozkąłdają się równomiernie między galaktyk i gwiazdy natomiast blade są praktycznie nie widoczne - wskazywać może to na miejsce do poprawy jako iż kategoria 'Dim' zdaje sie nie być użyteczna, a mogłaby posłużyć do lepszego objasnienia pozostałych. Podczerwienie i bliska podczerwień wskazują jedynie nieznacznie w stronę galaktyk - ten element utworzonych reguł jest albo typowy dla naszego zbioru, albo jego powiązanie z innymi cechami wskauzje na galaktyki, sam z siebie jest jednak niewystarczający.